查看原文
其他

我来给大家回答个问题

鸿洋 鸿洋 2020-10-29

上一篇:


今天想感谢一个伙伴


提出我在玩 Android开始更新问答了,当然也有同学留言。


不仅要有问答,还要有答案


说实话,太难办到了,因为什么呢?因为缺少时间。我现在每天大概就一个小时时间,23 点到 24 点,这一个小时主要用来看看文章,更新在网站文章以及问答。


不过偶尔回答一个问题还是可以的,我准备在周末有时间就写 1-2 个回答,当然为了严谨,我会尽可能通过源码和案例来辅证。


每日一问 自定义 ViewGroup 的时候,关于 LayoutParams 有哪些注意事项?


相信大家都看过非常多自定义控件的文章,我估计几乎所有人都能说出自定义的步骤。


但是实际上有很多细节,大家可能没有太关注。


这个问题,小缘的回答我就不贴了,回答的很全面了,可以去网站查看。


1. 复写 generateLayoutParams(AttributeSet attrs)


自定义 ViewGroup,它肯定有它特有的 layout 方式,很多时候需要依赖一些属性来完成,我们平时在xml 声明的 layout_开头的属性,其实都是该控件父控件的中 LayoutParams 所支持的属性。


例如:


LinearLayout.LayoutParams 的weight,gravity。


RelativeLayout.LayoutParam的各种 rules。


下面是 LinearLayout的源码:


@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) 
{
    return new LinearLayout.LayoutParams(getContext(), attrs);
}

public LayoutParams(Context c, AttributeSet attrs) {
    super(c, attrs);
    TypedArray a =
            c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);

    weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
    gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);

    a.recycle();
}


ok,这个大家应该都清楚。


2. 复写generateDefaultLayoutParams()


我们先写个 demo:


先声明一个 LinearLayout:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:id="@+id/ll_container"
    android:orientation="vertical"
    android:layout_height="match_parent"
    tools:context=".LayoutParamsActivity">


</LinearLayout>


然后动态 add 一个 View:


LinearLayout llContainer = findViewById(R.id.ll_container);

TextView tv  = new TextView(this);
tv.setText("哈哈哈");
tv.setTextColor(Color.WHITE);
tv.setBackgroundColor(Color.BLACK);

llContainer.addView(tv);


看一下效果图:



可以看到我们的 TextView 宽度:match_parent,高度 wrap_content 的一个状态。


这里我们修改一下 LinearLayout 的 orientation为horizontal:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:id="@+id/ll_container"
    android:orientation="horizontal">


</LinearLayout>


其他不不动,再运行一下:



可以看到,我们的 TextView 的宽高状态发生了变化,变成了 wrap_content,wrap_content


为什么会这样呢?


这里就引出第二个方法了,看一下 LinearLayout 的源码:


@Override
protected LayoutParams generateDefaultLayoutParams() {
    if (mOrientation == HORIZONTAL) {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    } else if (mOrientation == VERTICAL) {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    }
    return null;
}


可以看到generateDefaultLayoutParams()可以用于动态添加 View时,设置默认的LayoutParams 对象。


比如你在自定义一个 ViewGroup 的时候,是不是要思考下,哪种状态应该默认呢?是不是应该根据某个属性动态设置状态?


咱们继续。


3. 复写 generateLayoutParams(ViewGroup.LayoutParams lp)


这个方法就经常有同学忘了复写了。


有什么作用呢?

我从网上找了个 FlowLayout 的实现,大家不用在意细节,不过为了大家能够测试,我还是贴一下完整代码,大家阅读可以直接跳过:


public class FlowLayout extends ViewGroup {

    private static final int[] LL = new int[]{android.R.attr.maxLines};

    private List<List<View>> mAllViews = new ArrayList<List<View>>();
    private List<Integer> mLineHeight = new ArrayList<Integer>();

    private int mMaxLine = Integer.MAX_VALUE;

    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray a = context.obtainStyledAttributes(attrs, LL);
        mMaxLine = a.getInt(0, Integer.MAX_VALUE);
        a.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
{
        mAllViews.clear();
        mLineHeight.clear();

        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);


        int height = 0;
        int lineWidth = 0;

        int lineHeight = 0;

        int cCount = getChildCount();

        List<View> lineViews = new ArrayList<View>();

        for (int i = 0; i < cCount; i++) {

            View child = getChildAt(i);
            measureChild(child, widthMeasureSpec, heightMeasureSpec);

            MarginLayoutParams lp = (MarginLayoutParams) child
                    .getLayoutParams();
            int childWidth = child.getMeasuredWidth() + lp.leftMargin
                    + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin
                    + lp.bottomMargin;

            if (lineWidth + childWidth > sizeWidth - (getPaddingLeft() + getPaddingRight())) {
                // new line
                lineWidth = childWidth;
                height += lineHeight;

                mLineHeight.add(lineHeight);

                lineHeight = childHeight;

                mAllViews.add(lineViews);

                lineViews = new ArrayList<>();
                lineViews.add(child);

            } else {
                lineWidth += childWidth;
                lineHeight = Math.max(lineHeight, childHeight);
                lineViews.add(child);
            }

            if (i == cCount - 1) {
                height += lineHeight;
                mLineHeight.add(lineHeight);
                mAllViews.add(lineViews);
            }

        }

        if (mMaxLine < mLineHeight.size()) {
            height = getMaxLineHeight();
        }

        setMeasuredDimension(sizeWidth, (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight
                : height + getPaddingTop() + getPaddingBottom());

    }

    private int getMaxLineHeight() {
        int height = 0;
        for (int i = 0; i < mMaxLine; i++) {
            height += mLineHeight.get(i);
        }
        return height;
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) 
{

        int left = getPaddingLeft();
        int top = getPaddingTop();

        // 得到总行数
        int lineNums = mAllViews.size();
        for (int i = 0; i < lineNums; i++) {
            // 每一行的所有的views
            List<View> lineViews = mAllViews.get(i);
            // 当前行的最大高度
            int lineHeight = mLineHeight.get(i);
            // 遍历当前行所有的View
            for (int j = 0; j < lineViews.size(); j++) {
                View child = lineViews.get(j);
                if (child.getVisibility() == View.GONE) {
                    continue;
                }
                MarginLayoutParams lp = (MarginLayoutParams) child
                        .getLayoutParams();

                //计算childView的left,top,right,bottom
                int lc = left + lp.leftMargin;
                int tc = top + lp.topMargin;
                int rc = lc + child.getMeasuredWidth();
                int bc = tc + child.getMeasuredHeight();

                child.layout(lc, tc, rc, bc);

                left += child.getMeasuredWidth() + lp.rightMargin
                        + lp.leftMargin;
            }
            left = getPaddingLeft();
            top += lineHeight;
        }

    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) 
{
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() 
{
        return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

}


我们声明一下,写一小段简单的代码:


FlowLayout flowLayout = findViewById(R.id.flowlayout);

TextView tv  = new TextView(this);
tv.setText("哈哈哈");
tv.setTextColor(Color.WHITE);
tv.setBackgroundColor(Color.BLACK);

tv.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
   ViewGroup.LayoutParams.WRAP_CONTENT));

flowLayout.addView(tv);


运行:



java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams
        at com.imooc.imooc_flowlayout.FlowLayout.onMeasure(FlowLayout.java:62)
        at android.view.View.measure(View.java:16497)


会出现一个强转异常。


原因是,这里 TextView 设置的 LayoutParams 是 ViewGroup,而我们在 onMeasure 的时候,需要用过 child.getLayoutParams,直接转为 MarginLayoutParams 了。


那么是我们 onMeasure 的代码写的不规范吗?需要先 check 一下再强转?


相同的代码,我们把容器从 FlowLayout修改为 LinearLayout,再次运行:



并没有发生异常,很符合我们的预期。


是 LinearLayout 的 onMeasure 里面有 check 操作?


我们看一眼 LinearLayout 的 onMeasure 的源码:


void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {

    // ...
    final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
    totalWeight += lp.weight;

}


可以看到 LinearLayout 的内部也是直接强转的,为什么它没事。


这时候,你可能想到了这一节的标题,一定是要重写generateLayoutParams(ViewGroup.LayoutParams lp)方法。


那我们先在 FlowLayout 中重写下:


/**
     * add view cast
     *
     * @param p
     * @return
     */

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }


如果传入的为 ViewGroup.LayoutParams 的,我们手动转位 MarginLayoutParams。


修改代码为 FlowLayout.addView,运行一哈。



依然崩溃...


为什么呢?


接下来我们要去看源码了。


4. 源码之旅


直接点击 addView,ViewGroup.addView


public void addView(View child) {
    addView(child, -1);
}



public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}


注意看,如果 child.getLayoutParams 为 null,则会调用 generateDefaulyLayouyParams 方法,对应我们的第二节标题。


咱们继续:


public void addView(View child, int index, LayoutParams params) {
    requestLayout();
    invalidate(true);
    addViewInner(child, index, paramsfalse);
}

private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) 
{

    if (!checkLayoutParams(params)) {
        params = generateLayoutParams(params);
    }

    if (preventRequestLayout) {
        child.mLayoutParams = params;
    } else {
        child.setLayoutParams(params);
    }
//...
}


好像发现了,当:


if (!checkLayoutParams(params)) {
    params = generateLayoutParams(params);
}


原来,调用 generateLayout(params)方法,还需要一个条件 checkLayoutParams 返回 false。


那么我们刚才失效,说明这个方法默认返回 true 咯。


看第四个方法吧。


5. 复写checkLayoutParams(LayoutParams p)


默认实现:


protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
   return  p != null;
}


默认不为 null,就返回 true了。


那么我们FlowLayout需要的是 MarginLayoutParams,我们修改一下:


@Override
protected boolean checkLayoutParams(LayoutParams p) {
    return p instanceof MarginLayoutParams;
}


再次运行,终于符合预期了。



好了,以后遇到 LayoutParams 造成的 ClassCastException,可能不是你写代码的问题,而是那个封装控件的人,有些细节没有考虑到。


有了这个判断,我们就可以为所欲为,随意 setLayoutParams 吗?


我们再修改下代码:


FlowLayout flowLayout = findViewById(R.id.flowlayout);

TextView tv  = new TextView(this);
tv.setText("哈哈哈");
tv.setTextColor(Color.WHITE);
tv.setBackgroundColor(Color.BLACK);

flowLayout.addView(tv);

tv.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 
ViewGroup.LayoutParams.WRAP_CONTENT));


又崩溃咯:


java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams
        at com.imooc.imooc_flowlayout.FlowLayout.onMeasure(FlowLayout.java:62)


原因很简单了,addView 虽然父控件会帮你转化一下,但是一旦 addView 之后,父控件也没办法一直照顾着你...


好了,所以,当你下次自定义 ViewGroup的时候,需要考虑复写:


  • generateLayoutParams(AttributeSet attrs)

  • generateDefaultLayoutParams()

  • generateLayoutParams(LayoutParams p) 

  • checkLayoutParams(LayoutParams p) 


而且我们也从源码层看到这四个方法的作用。


最后我们也告诉了你,即使有这些方法,也不要随便 setLayoutParams...注意类型。


好了,以后就按照本文的文风给大家解答每日一问,会通过案例,源码保证回答的严谨性,有什么意见欢迎留言。


推荐阅读


教你写一个弹幕库,确定不了解一下?

Android 优质技术分享 | 6 期



扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存